<?php
#
# dmBridge: a data access framework for CONTENTdm(R)
#
# Copyright © 2009, 2010, 2011 Board of Regents of the Nevada System of Higher
# Education, on behalf of the University of Nevada, Las Vegas
#

/**
 * <p>A class consisting of helper methods to assist in creating web page
 * templates. The methods in this class are not specific to any particular
 * view and should work in all views.</p>
 *
 * <p>This class was called Draw in dmBridge 1.x.</p>
 *
 * @author Alex Dolski <alex.dolski@unlv.edu>
 * @license http://www.opensource.org/licenses/mit-license.php
 */
class DMGenericTemplateHelper extends DMAbstractTemplateHelper
implements DMTemplateHelper {

	/**
	 * @var array Array of arrays with 'type' and 'weight' keys, and either
	 * 'src' or 'code' keys
	 */
	private $body_scripts = array();

	/**
	 * @var array Array of arrays with 'src', 'type', and 'weight' keys
	 */
	private $head_scripts = array();

	/**
	 * @var array Array of arrays with 'name', 'content', and 'http-equiv' keys
	 */
	private $meta_tags = array();

	/**
	 * @var array Array of arrays with 'type', 'href', and 'media' keys
	 */
	private $stylesheets = array();

	/**
	 * Sorts the given array of scripts descending by weight.
	 * 
	 * @param int arr
	 */
	private static function sortScriptsByWeight(&$arr) {
		$did_swap = false;
		for ($i = 0, $count = count($arr); $i < $count; $i++) {
			if ($i < $count - 1) {
				if ($arr[$i]['weight'] < $arr[$i+1]['weight']) {
					$tmp = $arr[$i];
					$arr[$i] = $arr[$i+1];
					$arr[$i+1] = $tmp;
					$did_swap = true;
				}
			}
		}
		if ($did_swap) {
			self::sortScriptsByWeight($arr);
		}
	}

	/**
	 * @param DMAbstractView view
	 * @param DMSession session
	 */
	public function __construct(DMAbstractView $view,
			DMSession $session = null) {
		parent::__construct($view, $session);
		$this->addMetaTag("text/html; charset=utf-8", null, "Content-Type");
		$this->addMetaTag("public", null, "Cache-Control");
		$this->addMetaTag("public", null, "Pragma");
		$this->addMetaTag(DMConfigXML::getInstance()->getFeedCopyright(),
				"copyright");
	}

	/**
	 * <p>Assists in calling methods in template helpers and template helper
	 * modules. Not to be used publicly.</p>
	 *
	 * @param string method Method name
	 * @param array args Method arguments
	 */
	public function  __call($method, $args) {
		// check for non-module template helpers
		$all_helpers = $this->getView()->getAllHelpers();
		foreach ($all_helpers as $helper) {
			if (method_exists($helper, $method)) {
				return call_user_func_array(array($helper, $method), $args);
			}
		}
		throw new DMNoSuchMethodException($method);
	}

	/**
	 * Returns the HTML-escaped absolute URL to the Atom feed corresponding to
	 * the current view.
	 *
	 * @return string
	 * @since 0.1
	 */
	public function getHtmlAtomFeedURL() {
		return $this->getFeedURL("atom");
	}

	/**
	 * <p>Appends a script to the &lt;body&gt; script buffer, which can be
	 * accessed (e.g. for printing onto the page) via
	 * getHtmlBodyScriptTags().</p>
	 *
	 * <p>getHtmlBodyScriptTags() will output script in the order they were
	 * added. Scripts should therefore be added in dependency order, e.g.
	 * library scripts should be added before scripts which depend on them.</p>
	 *
	 * @param string src The publicly-accessible URI of the script. Should
	 * be an absolute URI path, if a local script.
	 * @param string type
	 * @param string media
	 * @param int weight
	 * @see addHeadScriptTag()
	 * @see removeBodyScriptTag()
	 */
	public function addBodyScriptCode($code, $type = "text/javascript",
			$weight = 0) {
		$this->body_scripts[] = array(
			'code' => $code,
			'type' => $type,
			'weight' => $weight
		);
	}

	/**
	 * <p>Appends a script to the &lt;body&gt; script buffer, which can be
	 * accessed (e.g. for printing onto the page) via
	 * getHtmlBodyScriptTags().</p>
	 *
	 * <p>getHtmlBodyScriptTags() will output script in the order they were
	 * added. Scripts should therefore be added in dependency order, e.g.
	 * library scripts should be added before scripts which depend on them.</p>
	 *
	 * @param string src The publicly-accessible URI of the script. Should
	 * be an absolute URI path, if a local script.
	 * @param string type
	 * @param string media
	 * @param int weight Integer from -99 to 99. Higher weights will be output
	 * before lower weights. Default is 0.
	 * @see addHeadScriptTag()
	 * @see removeBodyScriptTag()
	 */
	public function addBodyScriptTag($src, $type = "text/javascript",
			$weight = 0) {
		$tagvals = array(
			'src' => $src,
			'type' => $type,
			'weight' => $weight
		);

		// if the script already exists, overwrite it
		$count = count($this->body_scripts);
		for ($i = 0; $i < $count; $i++) {
			if (array_key_exists("src", $this->body_scripts[$i])) {
				if ($src == $this->body_scripts[$i]['src']) {
					$this->body_scripts[$i] = $tagvals;
					return;
				}
			}
		}

		// it doesn't exist, so add it
		$this->body_scripts[] = $tagvals;
	}

	/**
	 * @param array exclude_filenames Array of filenames to exclude from the
	 * output.
	 * @return string Series of HTML &lt;script&gt; elements.
	 * @todo Add $minify parameter
	 * @see addBodyScriptTag()
	 * @see addHeadScriptTag()
	 */
	public function getHtmlBodyScriptTags(array $exclude_filenames = array()) {
		$body_scripts = $this->body_scripts;
		self::sortScriptsByWeight($body_scripts);
		
		$tags = "";
		foreach ($body_scripts as $script) {
			if (array_key_exists("src", $script)) {
				if (!in_array(basename($script['src']), $exclude_filenames)) {
					$tags .= sprintf('<script type="%s" src="%s"></script>',
							$script['type'], $script['src']);
				}
			} else if (array_key_exists("code", $script)) {
				$tags .= sprintf('<script type="%s">%s</script>',
						$script['type'], $script['code']);
			}
		}
		return $tags;
	}

	/**
	 * Removes the script with the given src value.
	 *
	 * @param string src
	 * @return boolean True if the script was removed; false if the
	 * script was not found.
	 * @see addBodyScriptTag()
	 * @see removeHeadScriptTag()
	 */
	public function removeBodyScriptTag($src) {
		$count = count($this->body_scripts);
		for ($i = 0; $i < $count; $i++) {
			if ($src == $this->body_scripts[$i]['src']) {
				unset($this->body_scripts[$i]);
				return true;
			}
		}
		return false;
	}

	/**
	 * Returns an HTML &lt;select&gt; menu with links to all other authorized
	 * collections.
	 *
	 * @return string
	 * @since 0.4
	 */
	public function getHtmlCollectionsAsPulldown() {
		$cols = array();
		$cols = DMCollection::getAuthorized();
		$dxml = new DMDOMDocument("1.0", "utf-8");
		$dxml->loadXML("<form/>");
		$select = $dxml->createElement("select");
		$select->setAttribute("id", "dmSelectCollection");
		$select->setAttribute("class", "dmCollections");
		$select->setAttribute("onchange", "window.location = this.value");

		$option = $dxml->createElement("option", "Select a collection...");
		$select->appendChild($option);

		foreach ($cols as $c) {
			$query = new DMObjectQuery();
			$query->addCollection($c);
			$option = $dxml->createElement(
				"option", DMString::websafe($c->getName()));
			$option->setAttribute("value", $query->getURI());
			$collection = $this->getView()->getCollection();
			if ($collection) {
				if ($c->getAlias() == $collection->getAlias()) {
					$option->setAttribute("selected", "selected");
				}
			}
			$select->appendChild($option);
		}
		$dxml->documentElement->appendChild($select);

		$noscript = $dxml->createElement("noscript");
		$input = $dxml->createElement("input");
		$input->setAttribute("type", "submit");
		$input->setAttribute("value", "Go");
		$noscript->appendChild($input);
		$dxml->documentElement->appendChild($noscript);

		$dxml->formatOutput = true;
		return $dxml->saveHTML($dxml->documentElement);
	}

	/**
	 * Returns <strong>and erases</strong> the flash message from the user's
	 * session. The flash is returned as an &lt;p&gt; element with classes
	 * <code>dmFlash</code> and one of <code>dmSuccess</code>,
	 * <code>dmFailure</code>, or <code>dmNeutral</code>. Do not use this
	 * method to check for the presence of a flash message; use
	 * <code>DMSession::getFlash()</code> instead.
	 *
	 * @return string The flash message, in a classed &lt;p&gt; element.
	 * @since 0.1
	 */
	public function getHtmlFormattedFlash() {
		$tmp = DMHTTPRequest::getCurrent()->getSession()->getFlash();
		if (!$tmp instanceof DMFlash) {
			return false;
		}
		$flash = clone $tmp;
		DMHTTPRequest::getCurrent()->getSession()->unsetFlash();
		$status = "dmNeutral";
		if ($flash->getStatus() === true) {
			$status = "dmSuccess";
		} else if ($flash->getStatus() === false) {
			$status = "dmFailure";
		}
		$msg = sprintf('<p class="%s">%s</p>',
			trim("dmFlash " . $status),
			DMString::websafe($flash->getValue()));
		return $msg;
	}

	/**
	 * <p>Appends a script to the &lt;head&gt; script buffer, which can be
	 * accessed (e.g. for printing onto the page) via getHeadScripts().</p>
	 *
	 * <p>getHeadScripts() will output script in the order they were added.
	 * Scripts should therefore be added in dependency order, e.g. library
	 * scripts should be added before scripts which depend on them.</p>
	 *
	 * @param string src The publicly-accessible URI of the script. Should
	 * be an absolute URI path, if a local script.
	 * @param string type
	 * @param string media
	 * @see addBodyScriptTag()
	 * @see removeHeadScriptTag()
	 */
	public function addHeadScriptTag($src, $type = "text/javascript") {
		$tagvals = array(
			'src' => $src,
			'type' => $type
		);

		// if the script already exists, overwrite it
		$count = count($this->head_scripts);
		for ($i = 0; $i < $count; $i++) {
			if ($src == $this->head_scripts[$i]['src']) {
				$this->head_scripts[$i] = $tagvals;
				return;
			}
		}

		// it doesn't exist, so add it
		$this->head_scripts[] = $tagvals;
	}

	/**
	 * @param array exclude_filenames Array of filenames to exclude from the
	 * output.
	 * @return string Series of HTML &lt;script&gt; elements.
	 * @todo Add $minify parameter
	 * @see addHeadScriptTag()
	 * @see removeHeadScriptTag()
	 */
	public function getHtmlHeadScriptTags(array $exclude_filenames = array()) {
		$scripts = $this->head_scripts;
		self::sortScriptsByWeight($scripts);

		$tags = "";
		foreach ($scripts as $script) {
			if (!in_array(basename($script['src']), $exclude_filenames)) {
				$tags .= sprintf('<script type="%s" src="%s"></script>',
						$script['type'], $script['src']);
			}
		}
		return $tags;
	}

	/**
	 * Removes the stylesheet with the given src value. Use with caution as
	 * this may break certain functionality.
	 *
	 * @param string src
	 * @return boolean True if the script was removed; false if the
	 * script was not found.
	 * @see addHeadScriptTag()
	 */
	public function removeHeadScriptTag($src) {
		$count = count($this->head_scripts);
		for ($i = 0; $i < $count; $i++) {
			if ($src == $this->head_scripts[$i]['src']) {
				unset($this->head_scripts[$i]);
				return true;
			}
		}
		return false;
	}

	/**
	 * Conditionally returns a login or logout anchor tag depending on whether
	 * or not the user is logged in.
	 *
	 * @param string login_text
	 * @param string logout_text
	 * @return HTML anchor tag pointing to the relative URI to the login params
	 * @see loginURL()
	 * @see logoutURL()
	 * @since 0.1
	 */
	public function getHtmlLoginPageLink(
			$login_text = "Login", $logout_text = "Logout") {
		if (DMHTTPRequest::getCurrent()->getSession()->getUser()) {
			$dest = $this->getView()->getLogoutURI();
			$text = $logout_text;
		} else {
			$dest = $this->getView()->getLoginView()->getURI();
			$text = $login_text;
		}
		return sprintf('<a href="%s">%s</a>',
			DMString::websafe($dest), DMString::websafe($text));
	}

	/**
	 * Renders the login form.
	 *
	 * @return HTML string
	 * @since 2.0
	 */
	public function getHtmlLoginForm() {
		$html = sprintf('<form action="%s" method="post">
				<fieldset id="dmLoginForm">
					<legend>Login</legend>

					<label for="username">Username:</label>
					<input id="dmLoginUsername" type="text" name="username"
						size="20" maxlength="30">
					<br>

					<label for="password">Password:</label>
					<input id="dmLoginPassword" type="password" name="password"
						size="20" maxlength="30">
					<br>

					<input type="submit" value="Login">
				</fieldset>
			</form>',
			DMInternalURI::getURIWithParams("objects/login"));
		return $html;
	}

	/**
	 * <p>Appends a &lt;meta&gt; tag to the HTML &lt;head&gt; element. In
	 * general, only essential tags should be added.</p>
	 *
	 * <p>If a tag with the same name or http-equiv value already exist, it
	 * will be overwritten.</p>
	 *
	 * @param string content
	 * @param string name
	 * @param string http_equiv
	 * @see removeMetaTag()
	 */
	public function addMetaTag($content, $name, $http_equiv = null) {
		$tagvals = array(
			'content' => $content,
			'name' => $name,
			'http-equiv' => $http_equiv
		);

		// if the tag already exists, overwrite it
		$count = count($this->meta_tags);
		for ($i = 0; $i < $count; $i++) {
			if ($name == $this->meta_tags[$i]['name']
					|| $http_equiv == $this->meta_tags[$i]['http-equiv']) {
				$this->meta_tags[$i] = $tagvals;
				return;
			}
		}

		// it doesn't already exist, so add it
		$this->meta_tags[] = $tagvals;
	}

	/**
	 * @return string Series of HTML &lt;meta&gt; elements.
	 */
	public function getHtmlMetaTags() {
		$tags = "";
		foreach ($this->meta_tags as $mt) {
			$tag = sprintf('<meta content="%s"', $mt['content']);
			if ($mt['name']) {
				$tag .= sprintf(' name="%s"', $mt['name']);
			}
			if ($mt['http-equiv']) {
				$tag .= sprintf(' http-equiv="%s"', $mt['http-equiv']);
			}
			$tags .= $tag .= '>';
		}
		return $tags;
	}

	/**
	 * Removes the meta tag with the given name or http-equiv value.
	 *
	 * @param string name Optional name
	 * @param string http_equiv Optional http-equiv
	 * @return boolean True if the tag was removed; false if the tag was not
	 * found.
	 * @see addMetaTag()
	 */
	public function removeMetaTag($name = null, $http_equiv = null) {
		$count = count($this->meta_tags);
		for ($i = 0; $i < $count; $i++) {
			if ((!is_null($name) && $name == $this->meta_tags[$i]['name'])
					|| (!is_null($http_equiv) && $http_equiv == $this->meta_tags[$i]['http-equiv'])) {
				unset($this->meta_tags[$i]);
				return true;
			}
		}
		return false;
	}

	/**
	 * <p>Returns a hyperlinked ordered list of recently viewed objects, in
	 * descending chronological order.</p>
	 *
	 * @param int limit The maximum number of recently viewed objects to
	 * display.
	 * @param Boolean thumbs Whether or not to append thumbnails as
	 * &lt;img&gt; tags.
	 * @return string (X)HTML ordered list element
	 * @since 2.0
	 */
	public function getHtmlRecentlyViewedObjectsAsList($limit = 5, $thumbs = false) {
		$dxml = new DMDOMDocument("1.0", "utf-8");
		$dxml->loadXML("<ol/>");
		$dxml->documentElement->setAttribute("class", "dmRecentObjectLinks");

		$objects = $this->getView()->getRecentlyViewedObjects($limit);

		foreach (array_reverse($objects) as $obj) {
			$li = $dxml->createElement("li");
			$dxml->documentElement->appendChild($li);

			$a = $dxml->createElement("a");
			$a->setAttribute("href", $obj->getURI());

			$frag = $dxml->createDocumentFragment();
			if ($thumbs) {
				$img = $dxml->createElement("img");
				$img->setAttribute("class", "dmThumbnail");
				$img->setAttribute("src", $obj->getThumbnailURL());
				$img->setAttribute("alt", "Page thumbnail");
				$frag->appendChild($img);
			}
			$frag->appendChild($dxml->createTextNode(
					$obj->getMetadata("title")));
			$a->appendChild($frag);
			$li->appendChild($a);
		}

		return $dxml->saveHTML($dxml->documentElement);
	}
	
	/**
	 * Appends a stylesheet to the HTML &lt;head&gt; element in a &lt;link&gt;
	 * tag. If a stylesheet with a "href" identical to an existing stylesheet
	 * is added, it will overwrite the existing stylesheet.
	 *
	 * @param string href The publicly-accessible URI of the stylesheet. Should
	 * be an absolute URI path, if a local stylesheet.
	 * @param string type
	 * @param string media
	 * @see removeStylesheet()
	 */
	public function addStylesheetTag($href, $type = "text/css",
			$media = "screen") {
		$tagvals = array(
			'type' => $type,
			'media' => $media,
			'href' => $href
		);

		// if the stylesheet already exists, overwrite it
		$count = count($this->stylesheets);
		for ($i = 0; $i < $count; $i++) {
			if ($href == $this->stylesheets[$i]['href']) {
				$this->stylesheets[$i] = $tagvals;
				return;
			}
		}

		// it doesn't exist, so add it
		$this->stylesheets[] = $tagvals;
	}

	/**
	 * @return string Series of HTML &lt;link&gt; elements.
	 * @todo Add $minify parameter
	 */
	public function getHtmlStylesheetTags() {
		$tags = "";
		foreach ($this->stylesheets as $sheet) {
			$tags .= sprintf(
					'<link rel="stylesheet" type="%s" media="%s" href="%s">',
					$sheet['type'], $sheet['media'], $sheet['href']);
		}
		return $tags;
	}

	/**
	 * Removes the stylesheet with the given href value. Use with caution as
	 * this may break certain functionality.
	 *
	 * @param string href
	 * @return boolean True if the stylesheet was removed; false if the
	 * stylesheet was not found.
	 * @see addStylesheet()
	 */
	public function removeStylesheetTag($href) {
		$count = count($this->stylesheets);
		for ($i = 0; $i < $count; $i++) {
			if ($href == $this->stylesheets[$i]['href']) {
				unset($this->stylesheets[$i]);
				return true;
			}
		}
		return false;
	}

	/**
	 * Generates a string of CONTENTdm(R) field vocabulary as styleable (X)HTML
	 * anchor tags. Each anchor element has a class of "dmTag." If no tags
	 * exist, an empty string will be returned.
	 *
	 * @param int tag_limit
	 * @param int num_classes
	 * @param Boolean randomize Whether to return the tags in random order or
	 * in order by count
	 * @return string HTML anchor tags
	 */
	public function getHtmlTagsAsCloud($tag_limit = 20, $num_classes = 10,
			$randomize = true) {
		$tq = new DMTagQuery(DMDataStoreFactory::getDataStore());
		$tq->setNumResultsPerPage($tag_limit);
		$tq->addCollection($this->getView()->getCollection());
		$tq->setApproved(1);
		$tq->setSortByFrequency(true);

		$tags = $tq->getSearchResultsAsCounts();
		$counts = array_slice($tags, 0, $tag_limit, true);
		if (!count($counts)) {
			return "<!-- no results for cloud -->";
		}

		// calculate class numbering interval
		$min = min($counts);
		$max = max($counts);
		$diff = ($max - $min > 0) ? $max - $min : 1;
		$interval = ($num_classes - 1) / $diff;

		// generate the tags
		$tags = array();
		foreach ($counts as $value => $count) {
			// generate the query string
			$qs = array(
				'CISOBOX1' => $value,
				'CISOFIELD1' => "any",
				'CISOROOT' => $this->getView()->getCollection()->getAlias(),
				'CISOOP1' => "all"
			);
			$class_no = round(1 + ($count - $min) * $interval);
			$uri = DMInternalURI::getURIWithParams("objects", $qs);
			$tags[] = sprintf('<a class="dmTag dmTag%d" href="%s">%s</a>',
				$class_no,
				$uri,
				DMString::websafe($value));
			unset($qs);
		}
		return implode("\n", $tags);
	}

	/**
	 * @param string text_to_link
	 * @param DMObjectQuery query
	 * @return string HTML anchor tag
	 * @since 0.1
	 */
	public function getHtmlTermLinkedToSearch($text_to_link,
			DMObjectQuery $query) {
		// The basic idea is to build a DMObjectQuery based on the supplied
		// method parameters, and then use the return value of its getURI()
		// method.
		return sprintf('<a class="dmSearchTermLink" href="%s">%s</a>',
			$query->getURI(), $text_to_link);
	}

	/**
	 * Generates a string of CONTENTdm(R) field vocabulary as styleable HTML
	 * anchor tags. Each anchor element has a class of "dmTag dmTagX" where X
	 * may be a number from 1 to $num_classes.
	 *
	 * @param DMCollection collection
	 * @param array field_nicks Array of field nicknames (not DMDCElement
	 * objects)
	 * @param int tag_limit
	 * @param int num_classes Each tag will be returned with a class of
	 * dmTagX where X is an integer between 1 and $num_classes.
	 * @param sample_size An integer between 10 and 1024. A larger sample size
	 * is nice, but slower.
	 * @param Boolean randomize Whether to return the tags in random order or
	 * in order by count
	 * @return string HTML anchor tags
	 * @since 0.1
	 */
	public function getHtmlVocabularyAsCloud(array $field_nicks,
			$tag_limit = 20, $num_classes = 10, $sample_size = 512,
			$randomize = true) {
		/* Unfortunately cdm doesn't have a direct way of retrieving frequency
		data using the API. We will compile it ourselves here */
		$query = new DMObjectQuery();
		$query->addCollection($this->getView()->getCollection());
		$maxresults = ($sample_size > 1024) ? 1024 : $sample_size;
		$maxresults = ($sample_size < 10) ? 10 : $sample_size;
		$query->setNumResultsPerPage($maxresults);
		foreach ($field_nicks as $nick) {
			$predicate = new DMQueryPredicate();
			$predicate->setField(new DMDCElement($nick));
			$query->addPredicate($predicate);
		}
		$results = $query->getSearchResults();

		if (!count($results)) {
			return "<!-- no results for cloud -->";
		}

		// compile all unique terms in the $vocab array
		$vocab = array();
		foreach ($results as $obj) {
			foreach ($field_nicks as $nick) {
				$value = $obj->getField($nick);
				if ($value) {
					// split up semicolon-separated terms
					$tmp = explode("; ", $value);
					foreach ($tmp as &$t) {
						$t = rtrim(trim($t), ".,;");
						if (strlen($t) < 1) {
							unset($t);
						}
					}
					$vocab = array_merge($vocab, $tmp);
				}
			}
		}

		$counts = array_count_values($vocab);
		// enforce $tag_limit
		arsort($counts, SORT_NUMERIC);
		$counts = array_slice($counts, 0, $tag_limit, true);

		if (!count($counts)) {
			return "<!-- no results for cloud -->";
		}

		if ($randomize) {
			$tmp = array();
			$keys = array_keys($counts);
			shuffle($keys);
			foreach($keys as $key) {
				$tmp[$key] = $counts[$key];
				//unset($array[$key]);
			}
			$counts = $tmp;
		}

		// calculate class numbering interval
		$min = min($counts);
		$max = max($counts);
		$diff = ($max - $min > 0) ? $max - $min : 1;
		$interval = ($num_classes - 1) / $diff;

		// generate the tags
		$tags = array();
		foreach ($counts as $term => $c) {
			// generate the query string
			$qs = array(
				'CISOBOX1' => $term,
				'CISOFIELD1' => 'any',
				'CISOROOT' => $this->getView()->getCollection()->getAlias(),
				'CISOOP1' => 'all'
			);
			$class_no = round(1 + ($c - $min) * $interval);
			$uri = DMInternalURI::getURIWithParams("objects", $qs);
			$tags[] = sprintf('<a class="dmTag dmTag%d" href="%s">%s</a>',
				$class_no,
				$uri,
				DMString::websafe($term));
			unset($qs);
		}
		return implode("\n", $tags);
	}

	/**
	 * Returns an alphabetized list of controlled terms in a given collection's
	 * field, specified by <code>$field</code>.
	 *
	 * @param DMDCElement field
	 * @param Boolean linked If true, links each term to an "exact" search for
	 * objects in $field's collection having a field nick value matching the
	 * term.
	 * @return string Set of HTML list elements
	 * @since 0.1
	 */
	public function getHtmlVocabularyAsList(DMDCElement $field,
			$linked = true) {
		$vocab = $field->getVocabulary(false);
		natcasesort($vocab);

		$dxml = new DMDOMDocument("1.0", "utf-8");
		$dxml->loadXML("<ul/>");
		$dxml->documentElement->setAttribute("class", "dmVocabList");

		for ($i = 0; $i < count($vocab); $i++) {
			$li = $dxml->createElement('li');
			$li->setAttribute('class', 'dmVocabTerm');
			if ($linked) {
				$qs = array(
					'CISOBOX1' => $vocab[$i],
					'CISOFIELD1' => $field->getNick(),
					'CISOOP1' => "exact",
					'CISOROOT' => $field->getCollection()->getAlias()
				);
				$a = $dxml->createElement("a", DMString::websafe($vocab[$i]));
				$a->setAttribute("href",
					sprintf("/%s/?%s",
						DMString::websafe(trim(dirname($_SERVER['PHP_SELF']), "/")),
						DMString::websafe(urldecode(http_build_query($qs)))));
				$li->appendChild($a);
			} else {
				$li->nodeValue = DMString::websafe($vocab[$i]);
			}
			$dxml->documentElement->appendChild($li);
		}
		return $dxml->saveHTML($dxml->documentElement);
	}

	/**
	 * Returns an alphabetized list of controlled terms in a given collection's
	 * field, specified by $field, as HTML &lt;option&gt; elements.
	 *
	 * @param DMDCElement field
	 * @return string HTML &lt;option&gt; elements
	 * @since 0.4
	 */
	public function getHtmlVocabularyForSelect(DMDCElement $field) {
		$vocab = $field->getVocabulary(false);
		natcasesort($vocab);
		$terms = array();
		foreach ($vocab as $term) {
			$terms[] = sprintf("<option>%s</option>", DMString::websafe($term));
		}
		return implode("\n", $terms);
	}

	/**
	 * @param string format 'atom' or some other supported representation
	 * @return string
	 */
	protected function getFeedURL($format) {
		$uri = DMHTTPRequest::getCurrent()->getURI();
		$uri->setExtension($format);
		return $uri->__toString();
	}

}
